iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0
Software Development

軟體架構師的自我修養系列 第 14

[Day 14] 設計一個電商網站

  • 分享至 

  • xImage
  •  

從鐵人賽開賽至今,我們一直透過演化一個既有的系統來不斷提升各種非功能性需求。畢竟,架構師的任務就是不斷在各種取捨中找到相對最佳解,並且穩步將其推上線。

但有時候,我們會需要從無到有的把一個系統架設起來,這時我們需要思考的角度與演化系統會有些許不同。無中生有的過程必須要設想未來的可能性,而不是單純解決既有的問題,因此與演化系統相比,難度有過之而無不及。

這篇文章剛好是鐵人賽的一半,系統設計的章節會告一段落,接下來會講述各種軟體開發上的實務技巧。但在系統設計的最後,讓我們看看從無到有的過程又是如何。

這篇文章的目標是設計一個電商網站,這個題目比較通俗,因為是大家都有使用經驗的對象,舉凡會員功能、購物車功能、庫存功能等都很容易想像。

定義基本的領域服務

通常,對於一個新興專案,我都會建議從一個單體架構開始。在建立最簡可行產品(MVP)後,在開始根據各種商務需求進行演化,無論是微服務或事件驅動架構,都是從單體開始的。

透過一個能快速上線的單體進行概念驗證(POC)這是最保險的做法。

但是,對於電商網站來說,其實不需要快速驗證,因為電商的成敗取決於商品和行銷策略,而不是眾多客製化的商務邏輯。因此,設計電商網站的重點應該擺在可預見的未來內會碰到的問題。

以下列出一些實際的例子。

當使用者速度快速增加,管理使用者的需求也會顯著增加,那就有必要將使用者相關的功能變成獨立服務,以便在快速的開發週期中進行迭代。

庫存,當線上使用者增加,產品相關的功能會產生大量的流量,顧客總是會試著找尋心儀的商品和檢視其庫存。因此,為了處理大量的流量,我們有必要使庫存能夠水平擴展,因此庫存也會變成一個獨立服務。

另一個電商的核心功能是訂單管理和結帳。當顧客下單,外部的第三方服務就會進入結帳流程中,處理金流和物流等。而持續串接新的金流和物流也是無可避免的商務需求,因此訂單與結帳管理會有獨立的開發週期,並且需要能夠單獨進行整合和測試。也就是說,訂單也會變成一個服務。

最終,我們定義了基本的領域服務如下。

定義資料庫

前面我們提到需要有三個獨立服務,使用者、訂單和庫存。接著我們需要決定他們應該使用的資料庫。

當一個系統正要起步,我始終建議將成員最熟悉的幾個技術納入案例評估就好。不需要特別使用新潮的尖端科技。

因此,我相信MySQL應該會是對於訂單和庫存最好的選擇,因為他們都需要強一致性。儘管MySQL缺少好的水平擴展能力,但透過讀寫分離應足以應付起步的階段。若是未來讀寫分離也不夠用,也可以遷移至分散式SQL,至少對於應用程式來說,不需要改動太多。

至於使用者服務,這就有點有趣了。使用者相關管理功能通常不需要強一致性,且使用者相關資訊的結構很豐富多變,因此需要能良好支援各種結構的資料庫。我相信MongoDB會是個不錯的選擇,當然,也可以使用MySQL,不過就需要在資料正規化多花點心力。

此外,使用MongoDB若真有強一致性需求,例如提供使用者折價券,MongoDB依然擁有交易功能。不過,MongoDB的交易不太容易開發,會產生一些實作負擔。

定義通訊

現在我們有三個服務個別負責其所屬領域,但我們還沒決定他們是如何彼此溝通的。舉例來說,當一個顧客選擇了一項商品,接著下訂,緊接著結帳和物流,這三個服務必須要交互溝通來完成整個流程。

從我的角度來說,我推薦這三個服務透過非同步溝通。雖然整個流程必須確保資料一致性,這在非同步下非常難實現,但唯有非同步才有辦法達到高可用和高擴展。可用性和擴充性無疑是電商網站最重要的兩個非功能性需求。

例如,在黑色星期五的大促銷下,會有大量的顧客湧進網站,並且產生大量的訂單。假設三個服務用同步溝通,那麼三個服務必須用相同的速率進行水平擴展,這其實是很難達成的。

此外,當一個訂單要橫跨三個服務,如果任何一個服務暫時失效,在同步溝通下整個訂單都會無法進行。也就是說,唯有透過非同步溝通才能確保電商網站需要的高可用和高擴展。

到這為止,我們已經有了整個系統架構的雛形了。

但是,還有一個元件我們還沒決定好。那就是到底我們應該要用哪個訊息佇列?

我認為應該使用Kafka。為什麼不用傳統的訊息佇列,例如RabbitMQ?一個重要的理由是,Kafka具有極高吞吐量可以應付大促銷帶來的流量。

更有甚者,下單和結帳等行為是具有順序性的,當我們使用水平擴展訊息處理者以應付大流量時,為了要保證訊息之間的順序,必須使用Kafka的消費者群組機制。

唯有Kafka才有辦法做到既兼顧吞吐量又能保證順序。

工作流程概覽

一但我們有了系統架構,我們就可以描繪整個訂單的流程。

一開始,顧客在檢查完庫存後對著庫存服務下單,而庫存服務只負責處理庫存相關的任務,例如保留庫存。在訂單完成前先保留庫存有助於大量顧客下訂同一個商品時,不會有人跑完結帳流程才發現買到空氣。

當庫存服務處理完成後會通知訂單服務接手,並告知顧客的前端頁面進行跳轉。當訂單正確建立後,跳轉的前端頁面也可以看到訂單正在進行,並繼續處理交易的金流。

當訂單服務和金流都已完成,訂單服務會接著通知使用者服務。使用者服務會開始處理使用者的相關功能,例如折價券發放或者用戶等級提升。

最後庫存服務和訂單服務會被通知交易已經成功完成,庫存服務可以把庫存正式扣減,而訂單服務可以修改訂單狀態。

或者,如果有任何錯誤發生,出錯的服務必須要通知其他服務。如此一來,被通知的服務要進行對應的錯誤處理,舉例來說,庫存服務要把保留的庫存釋放。

但,這樣還不夠

我們總是提到各種面對錯誤的取捨。

昨天的系統設計中,有一個角色是crontab,用來定期檢查事件有沒有正確處理,若否,則要進行修復。

細節就不贅述,但我們知道,除了那三個領域服務外,還有一個監控服務來監視整個工作流程。

但,依然不夠

因為整個交易流程是非同步的,顧客必須要有辦法知道到底訂單現在的狀態是什麼、進行到哪個步驟等資訊。因此,必須要有一個服務訂閱各服務發出的所有訊息,並且保存在自己的資料庫內以便呈現交易狀態。

這樣的作法正是CQRS。

透過一個獨立跨領域的服務可以觀察完整的工作流程。但不像是監控者會參與交易流程進行修復作業,這個觀察者只是參與流程但不會做出任合修改。

觀察者的目的在於讓使用者更容易透過單一窗口了解工作流程的概況。

總結

電商網站可以被視為是所有後端架構的教科書,在電商網站上後端工程師會碰到幾乎所有種類的問題。但目前市售的書籍都只是提供一個單體架構,並把重點放在如何實作各種功能。

身為一個後端專家,我們都瞭解用一個單體來運行電商網站是死路一條。只要商品賣的出去,電商網站遲早會面臨大流量和可用性的挑戰。

雖然這篇文章並沒有完整的實作細節,但我們確實從各個角度看到電商網站的挑戰,也能夠體會,為什麼一個剛起步的電商網站就得要「五個」服務。

  • 為了更容易加入功能並且有獨立的整合和測試流程,必須要採用微服務架構。
  • 為了高可用和高擴展,服務間的耦合必須打破。
  • 為了更好容錯,一個有彈性的錯誤處理機制要被建立。
  • 為了有更好的使用體驗,必須要實作一個綜合呈現流程的地方。

之前我提到設計微服務架構困難度高且複雜,但不代表我完全拒絕使用微服務架構,相反的,我希望我們都能用正確的態度看待微服務,不是當作聖經也不是當作惡魔。

正確的取捨始終是系統設計最重要的核心概念。


上一篇
[Day 13] 用最小負擔實作事件驅動架構
下一篇
[Day 15] 單元測試和整合測試有什麼差別?
系列文
軟體架構師的自我修養31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
juck30808
iT邦研究生 1 級 ‧ 2022-09-15 00:57:38

看到工作流程圖真是回想起大學生活XD

lazypro iT邦新手 5 級 ‧ 2022-09-15 09:21:52 檢舉

學到的技能不會變啊,工作也是一直用那套/images/emoticon/emoticon08.gif
畢竟軟體工程已經成熟很久了

我要留言

立即登入留言